【速報】JUnit5 はこうなる!?【プロトタイプ】
渡辺です。 DevelopersIOでの100本目のエントリーがJUnitネタとなりました。
自分がJUnit実践入門を執筆したのは2011年から2012年にかけてです(出版が2012年11月)。 それからJava8がリリースされていますが、JUnit4自体は大きな進化はしていませんでした。
昨日、JUnit Lambda Prototypeが公開されました。 まだプロトタイプということで、今後の変更は大きいかと思いますが、いよいよ次世代のJUnitの足音が聞こえてきた感じがします。 今回は、このドキュメントからJUnit Lambdaの概要と方針について速報をお送りしたいと思います。
なお、現在JUnitチームでは、このプロトタイプに対するフィードバックを募集しています。 ここはこうじゃないとかはてブコメントする前にTwitterやGitHubでフィードバックを!
JUnit Lambda(JUnit5) について
JUnit Lambda(以後、JUnit5)はJUnit4から仕組みを一新し、Java8時代の新しいJUnitとして提供する方針になっています。 JUNit5の印象を3行で書くとこんな感じです。
- Lambda式対応したのでJava7以下はアウトオブ眼中
- 基本構造はJUnit4を継承したけど、中身はまったくの新ライブラリだからRuleとかTestRunnerとか忘れてくれ
- JUnit5ではMatcherはサポートしないから好きなの使ってね
Java系のプロダクトとしては、かなりイケイケな方針転換ですね(笑)
JUnitの歴史
折角なのでJUnitの歴史について簡単に解説しておきます。
現在主流であるJUnit4は、Java1.4時代に作られたJUnit3がベースであり、基本設計は非常に古いものです。 当然、アノテーションもありませんでした。 Java1.4の時代には、Javaのユニットテスティングフレームワークとして主流となります。
Java1.4からJava5への以降はかなり期間が空いたのですが、JUnit4のリリースもJava5のリリースからかなり後になります。 Java1.4時代の設計では厳しいので、Java5で導入されたアノテーション対応やユニットテストに必要な機能拡張などを行ったのがJUnit4です(ぶっちゃけ魔改造)。 このため、JUni4はユニットテストに必要な機能を多く提供していましたが、JUnit3との互換性や言語機能の制約などから使いにくい部分も多くあったと言えます。
一方、JUnit5はJava8以降をターゲットとしています。 JUnitの名前は残っていますが、完全に新しいテスティングフレームワークと言えます。 とはいえ、xUnitの系譜である点は変わりません。 基本的な部分をJUnit4から継承しつつ、機能の整理を行い、作り直したのがJUnit5です。
新しいアノテーションとJUnit5のテストコード
もう一度言いますが、JUnit5は、JUnit4とは全く異なるテスティングフレームワークです。 JUnit5では、JUnit4と同様に@Testアノテーションを宣言してテストメソッドを定義します。 しかし、JUnit5の@TestアノテーションとJUnit4の@Testアノテーションは、パッケージが異なります。
それでは、JUnit5のテストコードを見てみましょう。
import org.junit.gen5.api.*; @TestInstance(PER_CLASS) class MyTest { @BeforeAll void initAll() {} @BeforeEach void init() {} @Test void succeedingTest() {} @AfterEach void tearDown() {} @AfterAll void tearDownAll() {} }
JUnit5では、テストクラス・テストメソッド共にpublicである必要がなくなりました。 IDEを利用していれば大きな手間ではありませんが、タイピングが少なくなることは良いことでしょう。
テストクラスのライフサイクル - @TestInstance
@TestInstanceアノテーションはテストクラスに宣言し、テストクラスのライフサイクルについて定義します。 省略した場合も含め、デフォルトでは、JUnit4と同様に、テストメソッドの実行毎にテストクラスのインスタンスが作成されます。 一方、@TestInstance(PER_CLASS)を宣言した場合は、テストクラスのインスタンスは再利用され、テストメソッド毎に作成されません。
テストクラスの初期化と後処理 - @BeforeAll, @AfterAll
@BeforeAllアノテーションと@AfterAllアノテーションは、JUnit4の@BeforeClassアノテーションと@AfterClassアノテーションに対応するアノテーションです。 テストの初期化と後処理を行うメソッドを定義します。
JUnit5での変更点は、テストクラスを@TestInstance(PER_CLASS)で宣言した場合は、staticメソッドにする必要がない点です。 テストクラスを従来通りにテストメソッド毎に作成する場合は、staticメソッドにする必要があるので注意してください。
テストメソッドの初期化と後処理 - @BeforeEach, @AfterEach
@BeforeEachアノテーションと@AfterEachアノテーションは、JUnit4の@Beforeアノテーションと@Afterアノテーションに対応するアノテーションです。 JUnit4と挙動は変わらず、テストメソッドの実行前と実行後に実行するメソッドです。
テストメソッドを定義 - @Test
@Testアノテーションは、テストメソッドに定義するアノテーションです。 パッケージ以外に大きな変更点はありませんが、アノテーションのアトリビュートはすべて廃止されました。 テストに必要な属性はすべて別個のアノテーションとして宣言します。
アサーション
テストクラスとテストメソッドの構造は、若干の違いはあるもののJUnit4を踏襲しています。 一方、アサーションには大きく手が入っています。 すなわち、Lambda式対応とMatcherの廃止です。
アサーションメソッド
JUnit5のアサーションは、org.junit.gen5.api.Assertionsクラスのstaticメソッドとして提供されています。 基本的な使い方はJUnit4と変わりませんが、Lambda式に対応している点が大きく異なります。
assertEquals(2, 2, "エラーメッセージ(オプション)"); assertTrue(2 == 2, () -> "遅延" + "評価" + "されます。");
このようにassertEquals, assertTrueといったアサーションメソッドが提供されていますが、エラーメッセージ部分がLambda式に対応しているため、評価が失敗した場合にエラーメッセージ部分が評価されることになります。 今の所、このメリットとしては、toStringメソッドのコストを考慮する程度の使い方しか思いつきませんが・・・。
また、まとめてアサーションを行うassertAllメソッドが追加されました。
assertAll("address", () -> assertEquals("Johannes", address.getFirstName()), () -> assertEquals("Link", address.getLastName()) );
addressグループの中でアサーションが2回宣言されていますが、それぞれがLambda式になっています。
assertAllでは、最初のアサーションが失敗した場合でも、すべてのアサーションを評価するのがポイントです。 なんでもかんでも詰め込みすぎると「なんのテストを行うテストメソッドか解らない」状態となってしまいます。 しかし、1つ目のアサーションを通っても2つ目が通らなかったりすることは多々あるので、便利な機能です。 これはLambda式を有効に使っているパターンでしょう。
例外のテスト
例外のテストは、expectThrowsメソッドにテストの実行部分をLambda式として渡す事で実行できるようになりました(expectedはありません)。
Throwable exception = expectThrows(IllegalArgumentException.class, () -> sut.throwIllegalArgumentException("a message") ); assertEquals("a message", exception.getMessage());
expectThrowsメソッドの中で例外をcatchし、返してくれるワケですね! 例外メッセージの検証も容易です。
Matcherの廃止
ここまでassertTrueメソッドやassertEqualsメソッドを見てきましたが、assertThatメソッドは?と思った人は多いはずです。 JUnit4でHamcrestライブラリと合わせて導入されたassertThatメソッドはJUnit5には存在しません。 isメソッド(Isクラス)などのMatcherもありません。
実は、JUnit5ではMatcherは各プログラマが自由に選択する、というスタンスをとっています。 JUnit4で一時期は標準となったHamcrestを利用するのもひとつです。 この他に、Fest, AssertJ, TruthといったMatcherがあります。 JUnitはテストの枠組みを提供し、値の評価部分はサポートしないことになったようです(OLD Assertion API)。
JUnit5ではテストメソッドの実行時に例外が送出された場合、テスト失敗として扱います。
Ruleの廃止とJUnit5の拡張
JUnit4では多くの拡張機能がRuleやTestRunnerとして提供されました。 Ruleによりテストメソッドの振る舞いなどをカスタマイズすることができるようになり、ユニットテストの手法は大きく進化したと言えます。 でも、全部捨てましたw
しかし、実際は多くのRuleやTestRunnerが、JUnit5の基本機能として実装されています。
テスト名の利用 - @Name, @TestName
@Nameアノテーションと@TestNameアノテーションはテスト名を定義するアノテーションです。
@Test @Name("addメソッドに3と4を与えると7を返す") void add3_4(@TestName String testName) { fail(testName); }
@Nameアノテーションは、テストランナーやIDEが表示するテスト名を定義するアノテーションです。 Javaはメソッド名の制約が強いため、先頭に数字を入れたり、空白や句読点を含めることができません。 @Nameアノテーションに自由に文字列を入れることで回避することができます。
@TestNameアノテーションは、@TestNameルールに相当するアノテーションです。 Ruleはテストクラスのインスタンス変数に宣言する必要があり、非常に残念なテストコードになることはよく知られています。 JUnit5では、テストメソッドの引数などの制限も大幅に緩和されたため、@TestNameアノテーションをつけた引数を設定することで、テスト名をテストメソッド内で利用できます。
構造化テスト - @Nested
JUnit4では@Enclosedテストランナーを利用することでネストしたテストクラスを作成することができました。 ネストしたテストクラスは、テストを構造化する時に便利な機能です。 JUnit5では@Enclosedテストランナーは廃止されましたが、@Nestedアノテーションで同様の機能を提供します。
import org.junit.gen5.api.*; class MyObjectTest { MyObject myObject; @BeforeEach void init() { myObject = new MyObject(); } @Test void testEmptyObject() {} @Nested class WithChildren() { @BeforeEach void initWithChildren() { myObject.addChild(new MyObject()); myObject.addChild(new MyObject()); } @Test void testObjectWithChildren() {} } }
JUnit4との大きな違いは、ネストしたテストクラスが非staticなインナークラスとなったことです。 したがって、アウタークラスの変数にアクセスすることができます。 アウタークラス側にもテストメソッドを定義できるため、サンプルのように「空のオブジェクトに対するテスト」と、「オブジェクトに子を追加した場合のテスト」を構造化して定義することができるようになりました。
テストの拡張
JUnit5では、RuleやTestRunnerとは異なる形でテストを拡張することができます。 イメージとしてはTestRunnerに近く、テストの実行前などにフックを行い、共通の前処理などを行うことになります。
例えば、Mockitoの拡張を作ったならば次のように宣言します。
@ExtendWith(MockitoExtension.class) class MockTests { // ... }
実装例はこちらにありますが、テストの前処理やパラメータなどをハンドリングするメソッドを実装するだけなので、魔改造に近かったJUnitのTestRunnerより使いやすいでしょう。 特に、テストメソッドの引数が自由となり、拡張ポイントからパラメータを渡せるのは大きな変更点になります。
パラメータ化テスト
現時点ではパラメータ化テストに相当する機能はJUnit5のコア機能では提供されていません。 これも拡張機能を利用すれば実現する事は可能と思われます。 JUnit5のcore機能ではなく、拡張機能や外部ライブラリとして提供されるのでは?という感じです。
まとめ
JUnit5はJUnit4に似た部分はありますが、フレームワークとしては全く別のものとなっています。 アノテーションベースのJUnit4の構造を踏襲し、RuleやTestRunnerで無理矢理実装されていた機能を整理して作り直したフレームワークです。
基本的にユニットテストの手法を学んでいれば違和感なく使うことは可能かと思いますが、移行の過渡期では互換性の低さがネックになることは否めません。 今後、どのようなプロダクトになるかは解らない部分はありますが、現状はこんな感じで準備されています。